﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using VC1EncoderBackendLib;
using System.Diagnostics;

namespace ManagedVC1
{
    public class VC1Encoder
    {
        public delegate bool CallbackMethodDelegate(int StatusCode);

        private Thread m_EncoderThread;
        private bool m_RequestCancel;

        public bool IsCancelled { get; set; }

        public List<MediaSource> Sources { get; private set; }
        public string OutputFile { get; set; }
        public string ProfilePath { get; set; }
        
        // CONFIG_PROPERTIES
        public double BitRate { get; set; }
        public uint ComplexityLevel { get; set; }
        public uint QP { get; set; }
        public double FrameRate { get; set; }
        public bool InterlacedSource { get; set; }
        public uint MaxKeyFrameDistance { get; set; }
        public uint MaxHeight { get; set; }
        public uint MaxWidth { get; set; }
        public uint NumOfBFrames { get; set; }
        public double PeakBitRate { get; set; }
        public uint Profile { get; set; }
        public uint RateControlMode { get; set; }
        public uint VBVBufferInBytes { get; set; }

        //FILTERS
        public bool UseDenoiseFilter { get; set; }
        public bool UseInLoopFilter { get; set; }
        public bool UseMedianFilter { get; set; }
        public bool UseOverlapSmoothingFilter { get; set; }
        public bool UseNoiseEdgeRemoval { get; set; }

        //Motion Estimation (ME_PROPERTIES)
        public int DeltaMVRangeIndex { get; set; }
        public int MotionSearchLevel { get; set; }
        public int MotionSearchMethod { get; set; }
        public int MBModeCost { get; set; }
        public int MVCost { get; set; }
        public int MVRange { get; set; }

        //QUANT_PROPERTIES
        public int AdaptiveQuant { get; set; }
        public int DQuantOption { get; set; }
        public int DQuantPStrength { get; set; }
        public int DQuantBStrength { get; set; }

        //Other Options
        public bool LookAhead { get; set; }
        public int VideoType { get; set; }

        public event EventHandler<EncoderProgressEventArgs> ProgressChanged;
        public event EventHandler EncodingCompleted;
        public event EventHandler<EncoderFailedEventArgs> EncodingFailed;

        private int m_PriorStatus;
        private EncodeState m_CurState;

        public VC1Encoder()
        {
            Sources = new List<MediaSource>();

            //setup some defaults
            FrameRate = 29.97;
            ComplexityLevel = 3;
            InterlacedSource = false;
            NumOfBFrames = 0;
            Profile = 2;
            RateControlMode = 0;
            VideoType = 0;

            UseDenoiseFilter = false;
            UseInLoopFilter = false; //should be true for profile > 0
            UseMedianFilter = false;
            UseOverlapSmoothingFilter = false; //should be true for complexity > 0
            UseNoiseEdgeRemoval = false;

            DeltaMVRangeIndex = 0;
            MotionSearchLevel = 0;
            MotionSearchMethod = 0;
            MBModeCost = 0;
            MVCost = 0;
            MVRange = 0;

            AdaptiveQuant = 0;
            DQuantOption = 0;
            DQuantPStrength = 0;
            DQuantBStrength = 0;
        }

        public void Encode()
        {
            CheckForErrors();
            m_EncoderThread = new Thread(new ThreadStart(EncoderThread));
            m_EncoderThread.Start();
        }

        private void EncoderThread()
        {
            RunEncoder(true);
        }

        private void CheckForErrors()
        {
            if (String.IsNullOrEmpty(ProfilePath))
            {
                throw new Exception("Profile data not set");
            }

            if (!File.Exists(ProfilePath))
            {
                throw new FileNotFoundException("Unable to find profile at " + ProfilePath, ProfilePath);
            }

            if (Sources == null || Sources.Count == 0)
            {
                throw new Exception("No Sources found");
            }

            foreach (MediaSource src in Sources)
            {
                if (!File.Exists(src.FileName))
                {
                    throw new FileNotFoundException("Media source file was not found", src.FileName);
                }
            }
        }

        private void RunEncoder(bool runGraph)
        {
            string avsFile = GenerateAVSFile();

            m_RequestCancel = false;
            m_PriorStatus = 0;
            m_CurState = EncodeState.Initializing;
                        
            if (string.IsNullOrEmpty(avsFile))
            {
                return;
            }

            if (runGraph)
            {
                CallbackMethodDelegate progressCallback = new CallbackMethodDelegate(ProcessEncodeProgress);
                GCHandle functionHandle = GCHandle.Alloc(progressCallback);
                VC1EncASFClass vcEncoder = null;

                try
                {
                    vcEncoder = new VC1EncASFClass();
                    CONFIG_PROPERTIES2 props = GetConfigPropertes();
                    FILTERS2 filters = GetFilters();
                    ME_PROPERTIES2 meProperties = GetMEProperties();
                    QUANT_PROPERTIES2 quantProperties = GetQuantProperties();

                    vcEncoder.Init(props, filters, meProperties, quantProperties, Convert.ToInt32(LookAhead), VideoType, avsFile, ProfilePath, OutputFile);

                    IntPtr functionPointer = Marshal.GetFunctionPointerForDelegate(progressCallback);
                    vcEncoder.Encode(functionPointer.ToInt32());

                    //now the encoding has been cancelled - so make sure the user knows this
                    if (m_RequestCancel)
                    {
                        m_RequestCancel = false;
                        IsCancelled = true;
                    }

                    if (EncodingCompleted != null)
                    {
                        EncodingCompleted(this, EventArgs.Empty);
                    }
                }
                catch (Exception ex)
                {
                    if (EncodingFailed != null)
                    {
                        EncodingFailed(this, new EncoderFailedEventArgs(ex));
                    }
                }
                finally
                {
                    if (functionHandle != null && functionHandle.IsAllocated)
                    {
                        functionHandle.Free();
                    }

                    if (vcEncoder != null)
                    {
                        try
                        {
                            vcEncoder.Destroy();
                        }
                        catch { }
                        Marshal.FinalReleaseComObject(vcEncoder);
                    }
                    vcEncoder = null;
                }
            }

            try
            {
                //File.Delete(avsFile);
            }
            catch { } // don't really care if this fails

            m_CurState = EncodeState.Stopped;

        }

        public void CancelEncode()
        {
            m_RequestCancel = true;
        }

        private bool ProcessEncodeProgress(int statusCode)
        {
            if (m_RequestCancel)
            {
                return false;
            }
            //don't do anything if status really hasn't changed
            if (statusCode == m_PriorStatus)
                return true;
            switch(statusCode)
            {
                case 201:
                    Console.WriteLine("Begin Analyze");
                    m_CurState = EncodeState.Anaylzing;
                    break;
                case 202:
                    Console.WriteLine("End Anaylze");
                    break;
                case 203:
                    Console.WriteLine("Begin Encode");
                    m_CurState = EncodeState.Encoding;
                    break;
                case 204:
                    Console.WriteLine("End Encode");
                    break;
                default:
                    if (ProgressChanged != null)
                    {
                        //do two loops for anaylze and encode
                        if (statusCode > 100)
                            statusCode = statusCode - 100;

                        EncoderProgressEventArgs args = new EncoderProgressEventArgs(statusCode, m_CurState);
                        ProgressChanged(this, args);
                    }
                    break;
            }

            return true;
        }

        public string GenerateAVSFile()
        {
            return GenerateAVSFile(Colorspace.YV12);
        }
        public string GenerateAVSFile(Colorspace convColor)
        {
            List<string> srcFiles = new List<string>();
            StringBuilder strbld = new StringBuilder();
            int i = 0;
            bool hasInterlacedTrack = false;
            foreach (MediaSource s in Sources)
            {
                if (s.IsInterlaced)
                {
                    hasInterlacedTrack = true;
                    break;
                }
            }

            if (hasInterlacedTrack)
            {
                strbld.AppendLine("LoadPlugin(\"TDeint.dll\")");
            }

            foreach (MediaSource ms in Sources)
            {
                if (srcFiles.Contains(ms.FileName)) continue;

                //append source info
                //FFmpegSource available too
                strbld.Append("c");
                strbld.Append(i);
                strbld.Append("=");
                strbld.Append("DirectShowSource(\"");
                strbld.Append(ms.FileName);
                strbld.Append("\", fps=");
                if (ms.FrameRate > 0) strbld.AppendFormat(CultureInfo.InvariantCulture, "{0:F2}", ms.FrameRate); // some languages use commas instead of decimal points
                else strbld.AppendFormat(CultureInfo.InvariantCulture, "{0:F2}", this.FrameRate);

                strbld.Append(", audio=");
                strbld.Append(ms.IncludeAudio.ToString());

                strbld.AppendLine(")");

                srcFiles.Add(ms.FileName);
                i++;
            }

            i = 0;
            foreach (MediaSource ms in Sources)
            {
                if (i > 0) strbld.Append("+");

                strbld.AppendFormat("c{0}", srcFiles.IndexOf(ms.FileName));

                bool isNTSC = (ms.FrameRate == 0 || Math.Round(ms.FrameRate, 2) == 29.97);
                if (!isNTSC)
                {
                    strbld.Append(".Bob(height=480).BicubicResize(720,480).ConvertFPS(60000, 1001).SeparateFields.SelectEvery(4,0,3).Weave()");
                }

                if (ms.VideoResolution.Width == 352 && ms.VideoResolution.Height == 480)
                {
                    strbld.Append(".BicubicResize(640, 480)");
                }

                if (ms.IncludeAudio) strbld.Append(".ResampleAudio(44100)");

                strbld.AppendFormat(".Trim({0},{1})", ConvertTimeToFrame(ms.MarkIn), ConvertTimeToFrame(ms.MarkOut));

                if (ms.IsInterlaced)
                {
                    if (convColor != Colorspace.None)
                    {
                        strbld.Append(".ConvertToYV12()");
                    }
                    strbld.Append(".AssumeFrameBased().TDeint()");
                    if (convColor != Colorspace.None && convColor != Colorspace.YV12)
                    {
                        strbld.Append(".ConvertTo");
                        strbld.Append(convColor.ToString());
                        strbld.Append("()");
                    }
                }

                i++;
            }
            strbld.AppendLine();
            if (!hasInterlacedTrack && convColor != Colorspace.None)
            {
                strbld.Append("ConvertTo");
                strbld.Append(convColor.ToString());
                strbld.Append("()");
            }

            string filename = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".avs");
            File.WriteAllText(filename, strbld.ToString());

            return filename;
        }

        private int ConvertTimeToFrame(TimeSpan inputTime)
        {
            //if zero or not set then return zero
            //AVISynth will automatically set at beginning of clip (if markin) or end of clip (if markout)
            if (inputTime == TimeSpan.Zero || inputTime == TimeSpan.MinValue || inputTime == TimeSpan.MaxValue)
                return 0;

            //round to the next frame to avoid crazy scene change glitches with going to the previous frame
            return (int)Math.Ceiling(inputTime.TotalSeconds * this.FrameRate);
        }
 
        private CONFIG_PROPERTIES2 GetConfigPropertes()
        {
            ////error check
            if (RateControlMode == 1 || RateControlMode == 4)
            {
                VBVBufferInBytes = 0;
            }
            if (RateControlMode == 1)
            {
                BitRate = 0;
            }
            if (Profile != 2)
            {
                InterlacedSource = false;
            }
            if (RateControlMode == 3 || RateControlMode == 4)
            {
                QP = 0;
            }
            if (RateControlMode != 3)
            {
                PeakBitRate = 0;
            }

            if (RateControlMode != 0)
            {
                LookAhead = false;
            }

            return new CONFIG_PROPERTIES2()
            {
                bInterlacedSource = Convert.ToInt32(InterlacedSource),
                dBitRate = BitRate,
                dFrameRate = FrameRate,
                dPeakBitRate = PeakBitRate,
                dwComplexityLevel = ComplexityLevel,
                dwMaxHeight = MaxHeight,
                dwMaxKeyFrameDistance = MaxKeyFrameDistance,
                dwMaxWidth = MaxWidth,
                dwNumOfBFrames = NumOfBFrames,
                dwProfile = Profile,
                dwQP = QP,
                dwRateControlMode = RateControlMode,
                dwVBVBufferInBytes = VBVBufferInBytes
            };
        }

        private FILTERS2 GetFilters()
        {
            if (Profile == 0)
            {
                UseInLoopFilter = false;
            }

            return new FILTERS2()
            {
                bDenoise = Convert.ToInt32(UseDenoiseFilter),
                bInLoop = Convert.ToInt32(UseInLoopFilter),
                bMedian = Convert.ToInt32(UseMedianFilter),
                bNoiseEdgeRemoval = Convert.ToInt32(UseNoiseEdgeRemoval),
                bOverlapSmoothing = Convert.ToInt32(UseOverlapSmoothingFilter)
            };
        }

        private ME_PROPERTIES2 GetMEProperties()
        {
            return new ME_PROPERTIES2()
            {
                dwDeltaMVRangeIndex = (uint)DeltaMVRangeIndex,
                dwMBModeCost = (uint)MBModeCost,
                dwMotionSearchLevel = (uint)MotionSearchLevel,
                dwMotionSearchMethod = (uint)MotionSearchMethod,
                dwMVCost = (uint)MVCost,
                dwMVRange = (uint)MVRange
            };
        }

        private QUANT_PROPERTIES2 GetQuantProperties()
        {
            return new QUANT_PROPERTIES2()
            {
                dwAdaptiveQuant = (uint)AdaptiveQuant,
                dwDQuantBStrength = (uint)DQuantBStrength,
                dwDQuantOption = (uint)DQuantOption,
                dwDQuantPStrength = (uint)DQuantPStrength
            };
        }


    }
}
